package com.maylann.ratiocheater.encoder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 * by Maylann
 */
public class BEncode {

    private static Logger LOGGER = LoggerFactory.getLogger(BEncode.class);

    /**
     * Constant for UTF-8 String Encoding
     */
    public static final String UTF_8 = "UTF-8";

    public static final char LENGTH_DELIMITER_TOKEN = ':';
    public static final char DICTIONARY_TOKEN = 'd';
    public static final char LIST_TOKEN = 'l';
    public static final char NUMBER_TOKEN = 'i';
    public static final char END_TOKEN = 'e';

    /**
     * Decoding (DeBEncoding) the bencoded inputStream and return data in a map.
     *
     * @param in
     *          A Bencoded inputStream.
     * @return
     *          A Map containing the information from den bencoded inputstream.
     * @throws IOException
     */
    public static TorrentDictionary<String, Object> deBEncode(InputStream in) throws IOException {
        int token = read(in);

        if (token == DICTIONARY_TOKEN) {
            return parseDictionary(in);
        } else {
            throw new RuntimeException("Unable to deBEncode input. Stream should start with the dictionary");
        }
    }

    /**
     * BEncode {@link Object} to the {@link OutputStream}. Only these specific objects can be BEncoded here:
     * {@link Integer}
     * {@link String}
     * {@link byte[]}
     * {@link List}
     * {@link Map}
     *
     * @param object
     *          The {@link Object} to BEncode.
     * @param out
     *          The {@link OutputStream} the BEncoded data is written to.
     * @throws IOException
     */
    public static void bEncode(Object object, OutputStream out) throws IOException {
        if (object instanceof Number) {

            out.write(NUMBER_TOKEN);
            out.write(object.toString().getBytes());
            out.write(END_TOKEN);

        } else if (object instanceof String) {

            String s = (String) object;
            Integer q = s.getBytes().length;
            out.write(q.toString().getBytes());
            out.write(LENGTH_DELIMITER_TOKEN);
            out.write(s.getBytes());

        } else if (object instanceof List) {

            out.write(LIST_TOKEN);
            for (Object listObejct : (List) object) {
                bEncode(listObejct, out);
            }

            out.write(END_TOKEN);

        } else if (object instanceof TorrentDictionary) {

            out.write(DICTIONARY_TOKEN);
            TorrentDictionary<String, Object> dictionary = (TorrentDictionary<String, Object>) object;
            Set<String> keys = dictionary.keySet();

            for (String key : keys) {
                bEncode((key).getBytes(), out);
                bEncode(dictionary.get(key), out);
            }

            out.write(END_TOKEN);

        } else if (object instanceof byte[]) {

            byte[] bytes = (byte[]) object;
            Integer q = bytes.length;
            out.write(q.toString().getBytes());
            out.write(LENGTH_DELIMITER_TOKEN);
            out.write(bytes);

        } else {
            throw new RuntimeException("Unable to bEncode object. Unhandled parsing for object: " + object.getClass() );
        }
    }


    private static Object parseGeneral(InputStream in) throws IOException {
        int token = read(in);

        switch (token) {
            case LENGTH_DELIMITER_TOKEN:
                throw new RuntimeException("Error parsing BEncode. Shouldn't run into the length delimiter here");

            case DICTIONARY_TOKEN:
                return parseDictionary(in);

            case LIST_TOKEN:
                return parseList(in);

            case NUMBER_TOKEN:
                return parseNumber(in);

            case END_TOKEN:
                return null;

            default:
                if ((token >= '0') && (token <= '9')) {
                    return parseString(token, in);
                } else {
                    return null;
                    //throw new RuntimeException("Error parsing BEncode. Unknown token: " + token + "char:" + (char) token);
                }
        }
    }

    private static TorrentDictionary<String, Object> parseDictionary(InputStream in) throws IOException {
        TorrentDictionary<String, Object> map = new TorrentDictionary<String, Object>();
        while (true) {
            String key = (String) parseGeneral(in);
            if (key == null)
                return map;
            else {
                Object value;
                if (TorrentDictionary.PIECES.equals(key)) {
                    value = parseByteArray(in);
                } else {
                    value = parseGeneral(in);
                }
                map.put(key, value);
            }
        }
    }

    private static String parseString(int token, InputStream in) throws IOException {
        int length = findLength(token, in);
        return new String(parseBytes(length, in));
    }

    private static byte[] parseByteArray(InputStream in) throws IOException {
        int length = findLength(in.read(), in);
        return parseBytes(length, in);
    }

    private static byte[] parseBytes(int length, InputStream in) throws IOException {
        byte[] bytes = new byte[length];
        for (int i = 0; i < length; i++) {
            bytes[i] = (byte) read(in);
        }
        return bytes;
    }

    private static int findLength(int firstToken, InputStream in) throws IOException {
        StringBuilder digits = new StringBuilder();
        digits.append((char) firstToken);
        int token = read(in);
        while (token != LENGTH_DELIMITER_TOKEN) {
            digits.append((char) token);
            token = read(in);
        }

        return Integer.parseInt(digits.toString());
    }

    private static List<Object> parseList(InputStream in) throws IOException {
        List<Object> list = new ArrayList<Object>();
        while (true) {
            Object object = parseGeneral(in);
            if (object == null) {
                return list;
            } else {
                list.add(object);
            }
        }
    }

    private static Long parseNumber(InputStream in) throws IOException {
        int token = read(in);
        StringBuilder number = new StringBuilder();
        while (token != END_TOKEN) {
            number.append((char) token);
            token = read(in);
        }
        return Long.parseLong(number.toString().trim());
    }

    private static int read(InputStream in) throws IOException {
        int token = in.read();
        //LOGGER.debug("Token raw: " + token + " char: " + (char) token);
        return token;
    }
}
